/*
 * @(#)PopupWindow.java
 *
 * Copyright 2002 - 2003 JIDE Software. All rights reserved.
 */
package com.element.ui.window;

import com.element.util.UIUtil;

import javax.swing.*;
import javax.swing.event.EventListenerList;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import java.awt.*;
import java.awt.event.*;
import java.util.ArrayList;
import java.util.List;

/**
 * PopupWindow class
 * <p/>
 * You can add another JPopupMenu or JComboxBox in this popup.
 * <p/>
 * This class is copied from http://forum.java.sun.com/thread.jsp?forum=57&thread=230866 with some minor modifications.
 */
public class PopupWindow {
	/**
	 * A list of event listeners for this component.
	 */
	protected EventListenerList listenerList = new EventListenerList();

	private JWindow _delegate;
	private final Container _container;
	private final List<Component> _grabbed = new ArrayList<>();
	private final List<Component> _excluded = new ArrayList<>();
	private WindowListener _windowListener;
	private ComponentListener _componentListener;
	private ContainerListener _containerListener;
	private MouseListener _mouseListener;
	private Component _component;

	// JDK 1.3 Porting Hint.
	// Use AWTEventListener instead
	//    private AWTEventListener _keyEventDispatcher;
	private KeyEventDispatcher _keyEventDispatcher;

	private Component _parent;

	public PopupWindow(Container container) {
		_container = container;
		createDelegate();
		createListeners();
	}

	private void createDelegate() {
		Window window = getWindow();
		if (window != null) {
			_delegate = new JWindow(window);
		}
	}

	public void add(Component component) {
		_component = component;
		_component.addPropertyChangeListener("preferredSize", evt -> {
			if (_delegate != null) {
				_delegate.pack();
			}
		});
		if (_delegate != null) {
			_delegate.getContentPane().add(component);
			_delegate.pack();
			// workaround for a problem. JWindow somehow offset the height by 1
			// See http://developer.java.sun.com/developer/bugParade/bugs/4511106.html
			// looks like call pack again solve the problem.
			_delegate.pack();
//                mDelegate.setSize(mDelegate.getSize().width, mDelegate.getSize().height + 1);
		}
	}

	public void show(Component relative, int x, int y) {
		_parent = relative;
		if (_delegate == null) {
			createDelegate();
			if (_delegate == null) return;
			add(_component);
		}

		Point p = new Point(x, y);

		SwingUtilities.convertPointToScreen(p, relative);

		Rectangle screenSize = UIUtil.getScreenBounds(relative);

		Dimension size = _component.getPreferredSize();

		int left = p.x + size.width;
		int bottom = p.y + size.height;

		if (p.x < screenSize.x) {
			p.x = screenSize.x;
		}
		if (left > screenSize.width) {
			p.x = screenSize.width - size.width;
		}

		if (p.y < screenSize.y) {
			p.y = screenSize.y;
		}
		if (bottom > screenSize.height) {
			p.y = screenSize.height - size.height;
		}

//        Point location = relative.getLocationOnScreen();
		_delegate.setLocation(p.x, p.y);
		_delegate.setSize(_component.getPreferredSize());
		firePopupMenuWillBecomeVisible();
		_delegate.setVisible(true);
		grabContainers();

		// set popup window focus and register esc key
		// _delegate.toFront();
		// _delegate.requestFocus();

		// JDK 1.3 Porting Hint
		// Replace by AWTEventListener
		// Following Block is for JDK 1.3
		// _keyEventDispatcher = new AWTEventListener() {
		// 	public void eventDispatched(AWTEvent e) {
		// 		if (e instanceof KeyEvent) {
		// 			if (((KeyEvent) e).getKeyCode() == KeyEvent.VK_ESCAPE) {
		// 				hide();
		// 			}
		// 		}
		// 	}
		// };
		// Toolkit.getDefaultToolkit().addAWTEventListener(_keyEventDispatcher, AWTEvent.KEY_EVENT_MASK);

		// Following Block is for JDK 1.4
		_keyEventDispatcher = e -> {
			if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
				hide();
				return true;
			}
			return false;
		};
		DefaultFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher(_keyEventDispatcher);
	}

	public void hide() {
		if (_parent != null) {
			_parent.requestFocus();
		}
		firePopupMenuWillBecomeInvisible();

		if (_delegate != null) {
			_delegate.setVisible(false);
		}

		if (_keyEventDispatcher != null) {
			// JDK 1.3 Porting Hint
			// Replace by AWTEventListener
			// Toolkit.getDefaultToolkit().removeAWTEventListener(_keyEventDispatcher);
			DefaultFocusManager.getCurrentKeyboardFocusManager().removeKeyEventDispatcher(_keyEventDispatcher);
			_keyEventDispatcher = null;
		}
		releaseContainers();
		disposeDelegate();
	}

	private void createListeners() {
		_windowListener = new WindowAdapter() {
			@Override
			public void windowClosing(WindowEvent e) {
				hide();
			}

			@Override
			public void windowClosed(WindowEvent e) {
				hide();
			}

			@Override
			public void windowIconified(WindowEvent e) {
				hide();
			}
		};
		_componentListener = new ComponentListener() {
			public void componentResized(ComponentEvent e) {
				hide();
			}

			public void componentMoved(ComponentEvent e) {
				hide();
			}

			public void componentShown(ComponentEvent e) {
				hide();
			}

			public void componentHidden(ComponentEvent e) {
				hide();
			}
		};
		_containerListener = new ContainerListener() {
			public void componentAdded(ContainerEvent e) {
				hide();
			}

			public void componentRemoved(ContainerEvent e) {
				hide();
			}
		};
		_mouseListener = new MouseAdapter() {
			@Override
			public void mousePressed(MouseEvent e) {
				hide();
			}
		};
	}

	private void disposeDelegate() {
		if (_delegate != null) {
			_delegate.dispose();
			_delegate = null;
		}
	}

	private Window getWindow() {
		Container c = _container;
		if (c == null) {
			return null;
		}
		while (!(c instanceof Window) && c.getParent() != null) c = c.getParent();
		if (c instanceof Window) return (Window) c;
		return null;
	}

	private void grabContainers() {
		Container c = _container;
		while (!(c instanceof Window) && c.getParent() != null)
			c = c.getParent();
		grabContainer(c);
	}

	private void grabContainer(Container c) {
		if (c instanceof Window) {
			((Window) c).addWindowListener(_windowListener);
			c.addComponentListener(_componentListener);
			_grabbed.add(c);
		}

		synchronized (c.getTreeLock()) {
			int ncomponents = c.getComponentCount();
			Component[] component = c.getComponents();
			for (int i = 0; i < ncomponents; i++) {
				Component comp = component[i];
				if (!comp.isVisible()) continue;
				if (isExcludedComponent(comp)) {
					continue;
				}
				// TODO: this is not the right way to do things. Leave it for future enhancement to popup panel
				// don't hide popup when button of abstract combobox is pressed so that that button can toggle visibility of popup panel
				// if (comp instanceof AbstractButton && comp.getParent() instanceof AbstractComboBox) {
				// 	if (_delegate.isAncestorOf(((AbstractComboBox) comp.getParent()).getPopupPanel())) {
				// 		continue;
				// 	}
				// }
				comp.addMouseListener(_mouseListener);
				_grabbed.add(comp);
				if (comp instanceof Container cont) {
					if (cont instanceof JLayeredPane) {
						cont.addContainerListener(_containerListener);
					}
					grabContainer(cont);
				}
			}
		}
	}

	void releaseContainers() {
		for (Component c : _grabbed) {
			if (c instanceof Window) {
				((Window) c).removeWindowListener(_windowListener);
				c.removeComponentListener(_componentListener);
			} else {
				c.removeMouseListener(_mouseListener);
			}

			if (c instanceof Container) {
				if (c instanceof JLayeredPane) {
					((Container) c).removeContainerListener(_containerListener);
				}
			}
		}
		_grabbed.clear();
	}

	/**
	 * Gets the visibility of this popup.
	 *
	 * @return true if popup is visible
	 */
	public boolean isVisible() {
		return _delegate != null && _delegate.isVisible();
	}

	/**
	 * Adds a <code>PopupMenu</code> listener which will listen to notification messages from the popup portion of the
	 * combo box.
	 * <p/>
	 * For all standard look and feels shipped with Java 2, the popup list portion of combo box is implemented as a
	 * <code>JPopupMenu</code>. A custom look and feel may not implement it this way and will therefore not receive the
	 * notification.
	 *
	 * @param l the <code>PopupMenuListener</code> to add
	 * @since 1.4
	 */
	public void addPopupMenuListener(PopupMenuListener l) {
		listenerList.add(PopupMenuListener.class, l);
	}

	/**
	 * Removes a <code>PopupMenuListener</code>.
	 *
	 * @param l the <code>PopupMenuListener</code> to remove
	 * @see #addPopupMenuListener
	 * @since 1.4
	 */
	public void removePopupMenuListener(PopupMenuListener l) {
		listenerList.remove(PopupMenuListener.class, l);
	}

	/**
	 * Returns an array of all the <code>PopupMenuListener</code>s added to this JComboBox with addPopupMenuListener().
	 *
	 * @return all of the <code>PopupMenuListener</code>s added or an empty array if no listeners have been added
	 * @since 1.4
	 */
	public PopupMenuListener[] getPopupMenuListeners() {
		return listenerList.getListeners(PopupMenuListener.class);
	}

	/**
	 * Notifies <code>PopupMenuListener</code>s that the popup portion of the combo box will become visible.
	 * <p/>
	 * This method is public but should not be called by anything other than the UI delegate.
	 *
	 * @see #addPopupMenuListener
	 * @since 1.4
	 */
	public void firePopupMenuWillBecomeVisible() {
		Object[] listeners = listenerList.getListenerList();
		PopupMenuEvent e = null;
		for (int i = listeners.length - 2; i >= 0; i -= 2) {
			if (listeners[i] == PopupMenuListener.class) {
				if (e == null)
					e = new PopupMenuEvent(this);
				((PopupMenuListener) listeners[i + 1]).popupMenuWillBecomeVisible(e);
			}
		}
	}

	/**
	 * Notifies <code>PopupMenuListener</code>s that the popup portion of the combo box has become invisible.
	 * <p/>
	 * This method is public but should not be called by anything other than the UI delegate.
	 *
	 * @see #addPopupMenuListener
	 * @since 1.4
	 */
	public void firePopupMenuWillBecomeInvisible() {
		Object[] listeners = listenerList.getListenerList();
		PopupMenuEvent e = null;
		for (int i = listeners.length - 2; i >= 0; i -= 2) {
			if (listeners[i] == PopupMenuListener.class) {
				if (e == null)
					e = new PopupMenuEvent(this);
				((PopupMenuListener) listeners[i + 1]).popupMenuWillBecomeInvisible(e);
			}
		}
	}

	/**
	 * Notifies <code>PopupMenuListener</code>s that the popup portion of the combo box has been canceled.
	 * <p/>
	 * This method is public but should not be called by anything other than the UI delegate.
	 *
	 * @see #addPopupMenuListener
	 * @since 1.4
	 */
	public void firePopupMenuCanceled() {
		Object[] listeners = listenerList.getListenerList();
		PopupMenuEvent e = null;
		for (int i = listeners.length - 2; i >= 0; i -= 2) {
			if (listeners[i] == PopupMenuListener.class) {
				if (e == null)
					e = new PopupMenuEvent(this);
				((PopupMenuListener) listeners[i + 1]).popupMenuCanceled(e);
			}
		}
	}

	/**
	 * PopupWindow will add necessary listeners to some components so that mouse click etc can hide the popup window.
	 * However in certain case, you might not want this.
	 *
	 * @param comp component which will not hide popup when it is clicked.
	 */
	public void addAsExcludedComponents(Component comp) {
		if (_excluded.contains(comp)) {
			return;
		}
		_excluded.add(comp);
	}

	public void removeFromExcludedComponents(Component comp) {
		if (!_excluded.contains(comp)) {
			return;
		}
		_excluded.remove(comp);
	}

	public boolean isExcludedComponent(Component comp) {
		return _excluded.contains(comp);
	}
}
